defmodule Plausible.InstallationSupport.Check do
  @moduledoc """
  Behaviour to be implemented by a specific installation support check.

  `report_progress_as()` doesn't necessarily reflect the actual check
  description, it serves as a user-facing message grouping mechanism,
  to prevent frequent message flashing when checks rotate often.

  Each check operates on `%Plausible.InstallationSupport.State{}` and is
  expected to return it, optionally modified, by all means. `perform_safe/1`
  is used to guarantee no exceptions are thrown by faulty implementations,
  not to interrupt LiveView.
  """
  @type state() :: Plausible.InstallationSupport.State.t()
  @callback report_progress_as() :: String.t()
  @callback perform(state(), Keyword.t()) :: state()

  defmacro __using__(_) do
    quote do
      import Plausible.InstallationSupport.State
      alias Plausible.InstallationSupport.State

      require Logger

      @behaviour Plausible.InstallationSupport.Check

      def perform_safe(state, opts) do
        timeout = Keyword.get(opts, :timeout, 10_000)

        task =
          Task.async(fn ->
            try do
              perform(state, opts)
            catch
              _, e ->
                Logger.error(
                  "Error running check #{inspect(__MODULE__)} on #{state.url}: #{inspect(e)}"
                )

                put_diagnostics(state, service_error: %{code: :internal_check_error, extra: e})
            end
          end)

        try do
          Task.await(task, timeout)
        catch
          :exit, {:timeout, _} ->
            Task.shutdown(task, :brutal_kill)
            check_name = __MODULE__ |> Atom.to_string() |> String.split(".") |> List.last()

            put_diagnostics(state,
              service_error: %{
                code: :internal_check_timeout,
                extra: "#{check_name} timed out after #{timeout}ms"
              }
            )
        end
      end
    end
  end
end
